package service

import (
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"gitee.com/actor168/gitserver/pkg/config"
	"gitee.com/actor168/gitserver/pkg/repoutil"
	"github.com/gogs/git-module"
	"github.com/pkg/errors"
)

func (g *gitRepository) prepareMergedViewPullInfo(pr *PullRequest) (NumInfo, error) {
	headRepoPath := filepath.Join(config.Conf.Dir, pr.HeadRepo) + ".git"
	var (
		headGitRepo *git.Repository
		err         error
	)

	if pr.HeadRepo != "" {
		headGitRepo, err = git.Open(headRepoPath)
		if err != nil {
			return NumInfo{}, errors.Errorf("open repository:%v", err)
		}
	}

	NumCommits, err := headGitRepo.RevListCount([]string{pr.MergeBase + "..." + pr.MergeCommitId})
	if err != nil {
		return NumInfo{}, err
	}

	names, err := headGitRepo.DiffNameOnly(pr.MergeBase, pr.MergeCommitId, git.DiffNameOnlyOptions{NeedsMergeBase: true})
	NumFiles := len(names)
	if err != nil {
		return NumInfo{}, err
	}
	return NumInfo{
		NumCommits: int(NumCommits),
		NumFiles:   NumFiles,
	}, nil
}

func (g *gitRepository) viewPullInfo(pr *PullRequest) (*PullRequestMeta, error) {
	baseRepoPath := filepath.Join(config.Conf.Dir, pr.BaseRepo) + ".git"
	headRepoPath := filepath.Join(config.Conf.Dir, pr.HeadRepo) + ".git"
	var (
		headGitRepo *git.Repository
		err         error
	)

	if pr.HeadRepo != "" {
		headGitRepo, err = git.Open(headRepoPath)
		if err != nil {
			return nil, errors.Errorf("open repository:%v", err)
		}
	}

	if pr.HeadRepo == "" || !headGitRepo.HasBranch(pr.HeadBranch) {
		return nil, errors.New("no branch")
	}
	prMeta, err := pullRequestMeta(headRepoPath, baseRepoPath, pr.HeadBranch, pr.BaseBranch)
	if err != nil {
		if strings.Contains(err.Error(), "fatal: Not a valid object name") {
			return nil, nil
		}
		return nil, errors.New("err")
	}
	return prMeta, nil
}

func pullRequestMeta(headPath, basePath, headBranch, baseBranch string) (*PullRequestMeta, error) {
	tmpRemoteBranch := baseBranch

	// We need to create a temporary remote when the pull request is sent from a forked repository.
	if headPath != basePath {
		tmpRemote := strconv.FormatInt(time.Now().UnixNano(), 10)
		err := git.RemoteAdd(headPath, tmpRemote, basePath, git.RemoteAddOptions{Fetch: true})
		if err != nil {
			return nil, fmt.Errorf("add remote: %v", err)
		}
		defer func() {
			err := git.RemoteRemove(headPath, tmpRemote)
			if err != nil {
				return
			}
		}()

		tmpRemoteBranch = "remotes/" + tmpRemote + "/" + baseBranch
	}

	mergeBase, err := git.MergeBase(headPath, tmpRemoteBranch, headBranch)
	if err != nil {
		return nil, errors.Wrap(err, "get merge base")
	}

	commits, err := git.Log(headPath, mergeBase+"..."+headBranch)
	if err != nil {
		return nil, errors.Wrap(err, "get commits")
	}

	// Count number of changed files
	names, err := git.DiffNameOnly(headPath, tmpRemoteBranch, headBranch, git.DiffNameOnlyOptions{NeedsMergeBase: true})
	if err != nil {
		return nil, errors.Wrap(err, "get changed files")
	}

	return &PullRequestMeta{
		MergeBase: mergeBase,
		Commits:   commits,
		NumFiles:  len(names),
	}, nil
}

func parseCompareInfo(pr *PRCompareReqModel) (*git.Repository, *PullRequestMeta, string, []string, []string, error) {

	// Get compared branches information
	// format: <base branch>...[<head repo>:]<head branch>
	// base<-head: master...head:feature
	// same repo: master...feature
	//infos := strings.Split(c.Params("*"), "...")
	//
	//if len(infos) != 2 {
	//	log.Trace("ParseCompareInfo[%d]: not enough compared branches information %s", "", infos)
	//	c.NotFound()
	//	return nil, nil, ""
	//}

	baseBranch := pr.Before

	var (
		headBranch   string
		headRepoName string
		headUser     string
		isSameRepo   bool
		err          error
	)

	headInfos := strings.Split(pr.After, ":")
	if len(headInfos) == 1 {
		isSameRepo = true
		headBranch = headInfos[0]
	} else if len(headInfos) == 3 {
		headUser = headInfos[0]
		headRepoName = headInfos[1]
		headBranch = headInfos[2]
		isSameRepo = false
	} else {
		return nil, nil, "", nil, nil, errors.New("not found")
	}

	// Check if base branch is valid.
	baseRepoPath := repoutil.RepoPath(pr.Code, pr.RepoName)
	baseRepo, err := git.Open(baseRepoPath)
	if err != nil || !baseRepo.HasBranch(baseBranch) {
		return nil, nil, "", nil, nil, errors.New("no branch or repo")
	}
	baseBranchs, err := baseRepo.Branches()
	if err != nil {
		return nil, nil, "", nil, nil, errors.New("get branches err")
	}

	var headGitRepo *git.Repository

	// In case user included redundant head user name for comparison in same repository,
	// no need to check the fork relation.
	if !isSameRepo {
		headRepoPath := repoutil.RepoPath(headUser, headRepoName)
		headGitRepo, err = git.Open(headRepoPath)
		if err != nil {
			return nil, nil, "", nil, nil, errors.New("open repository err")
		}
	} else {
		headGitRepo = baseRepo
	}

	// Check if head branch is valid.
	if !headGitRepo.HasBranch(headBranch) {
		return nil, nil, "", nil, nil, errors.New("no branch")
	}

	headBranches, err := headGitRepo.Branches()
	if err != nil {
		return nil, nil, "", nil, nil, errors.New("get branches err")
	}

	meta, err := pullRequestMeta(headGitRepo.Path(), baseRepoPath, headBranch, baseBranch)
	if err != nil {
		return nil, nil, "", nil, nil, err
	}

	return headGitRepo, meta, headBranch, baseBranchs, headBranches, nil
}

func prepareCompareDiff(
	headGitRepo *git.Repository,
	meta *PullRequestMeta,
	headBranch string,
) (bool, *Diff, string, []*git.Commit, error) {
	var (
		err           error
		afterCommitID string
	)

	// Get diff information.

	headCommitID, err := headGitRepo.BranchCommitID(headBranch)
	if err != nil {
		return false, nil, "", nil, errors.New("get head branch commit ID err")
	}
	afterCommitID = headCommitID

	if headCommitID == meta.MergeBase {
		return true, nil, "", nil, nil
	}

	diff, err := headGitRepo.Diff(
		headCommitID, config.Conf.MaxDiffFiles, config.Conf.MaxDiffLines, config.Conf.MaxDiffLineChars,
		git.DiffOptions{Base: meta.MergeBase, Timeout: time.Duration(config.Conf.DiffTimeOut) * time.Second},
	)
	if err != nil {
		return false, nil, "", nil, errors.New("get repository diff err")
	}

	return false, NewDiff(diff), afterCommitID, meta.Commits, nil
}

func getIndex(code, repoName string) (int, error) {
	repoPath := repoutil.RepoPath(code, repoName)
	dir := filepath.Join(repoPath, "pulls")
	_, err := os.Stat(dir)
	if err == nil {
		files, err := os.ReadDir(dir)
		if err != nil {
			return 0, errors.New("读取目录出错")
		}
		count := 0
		for _, file := range files {
			if !file.IsDir() {
				count++
			}
		}
		return count + 1, nil
	} else if os.IsNotExist(err) {
		return 1, nil
	}
	return 0, errors.New("其他错误")
}

// SavePatch saves patch data to corresponding location by given issue ID.
func savePatch(index int, patch []byte, repoLink string) error {
	patchPath, err := PatchPath(index, repoLink)
	if err != nil {
		return fmt.Errorf("PatchPath: %v", err)
	}

	if err = os.MkdirAll(filepath.Dir(patchPath), os.ModePerm); err != nil {
		return err
	}
	if err = os.WriteFile(patchPath, patch, 0644); err != nil {
		return fmt.Errorf("WriteFile: %v", err)
	}

	return nil
}

func (g *gitRepository) ListPRInfo(req *PRCommitReqModel) *GitRespModel {
	//PrepareViewPullInfo
	var (
		numInfo NumInfo
		err     error
	)
	if req.PR.HasMerged {
		numInfo, err = g.prepareMergedViewPullInfo(req.PR)
		if err != nil {
			return formatResp(500, err.Error(), nil)
		}
	} else {
		prMeta, err := g.viewPullInfo(req.PR)
		if err != nil {
			return formatResp(500, err.Error(), nil)
		}
		if prMeta == nil {
			numInfo = NumInfo{
				NumCommits: 0,
				NumFiles:   0,
			}
		} else {
			numInfo = NumInfo{
				NumCommits: len(prMeta.Commits),
				NumFiles:   prMeta.NumFiles,
			}
		}
	}
	return formatResp(200, "OK", numInfo)
}
